{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# MCP + LangGraph 핸즈온 튜토리얼\n",
    "\n",
    "- 작성자: [테디노트](https://youtube.com/c/teddynote)\n",
    "- 강의: [패스트캠퍼스 RAG 비법노트](https://fastcampus.co.kr/data_online_teddy)\n",
    "\n",
    "**참고자료**\n",
    "- https://modelcontextprotocol.io/introduction\n",
    "- https://github.com/langchain-ai/langchain-mcp-adapters"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 환경설정\n",
    "\n",
    "아래 설치 방법을 참고하여 `uv` 를 설치합니다.\n",
    "\n",
    "**uv 설치 방법**\n",
    "\n",
    "```bash\n",
    "# macOS/Linux\n",
    "curl -LsSf https://astral.sh/uv/install.sh | sh\n",
    "\n",
    "# Windows (PowerShell)\n",
    "irm https://astral.sh/uv/install.ps1 | iex\n",
    "```\n",
    "\n",
    "**의존성 설치**\n",
    "\n",
    "```bash\n",
    "uv pip install -r requirements.txt\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "환경변수를 가져옵니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from dotenv import load_dotenv\n",
    "\n",
    "load_dotenv(override=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## MultiServerMCPClient"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "사전에 `mcp_server_remote.py` 를 실행해둡니다. 터미널을 열고 가상환경이 활성화 되어 있는 상태에서 서버를 실행해 주세요.\n",
    "\n",
    "> 명령어\n",
    "```bash\n",
    "source .venv/bin/activate\n",
    "python mcp_server_remote.py\n",
    "```\n",
    "\n",
    "`async with` 로 일시적인 Session 연결을 생성 후 해제"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_mcp_adapters.client import MultiServerMCPClient\n",
    "from langgraph.prebuilt import create_react_agent\n",
    "from utils import ainvoke_graph, astream_graph\n",
    "from langchain_anthropic import ChatAnthropic\n",
    "\n",
    "model = ChatAnthropic(\n",
    "    model_name=\"claude-3-7-sonnet-latest\", temperature=0, max_tokens=20000\n",
    ")\n",
    "\n",
    "async with MultiServerMCPClient(\n",
    "    {\n",
    "        \"weather\": {\n",
    "            # 서버의 포트와 일치해야 합니다.(8005번 포트)\n",
    "            \"url\": \"http://localhost:8005/sse\",\n",
    "            \"transport\": \"sse\",\n",
    "        }\n",
    "    }\n",
    ") as client:\n",
    "    print(client.get_tools())\n",
    "    agent = create_react_agent(model, client.get_tools())\n",
    "    answer = await astream_graph(agent, {\"messages\": \"서울의 날씨는 어떠니?\"})"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "다음의 경우에는 session 이 닫혔기 때문에 도구에 접근할 수 없는 것을 확인할 수 있습니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "await astream_graph(agent, {\"messages\": \"서울의 날씨는 어떠니?\"})"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "이제 그럼 Async Session 을 유지하며 도구에 접근하는 방식으로 변경해 보겠습니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 1. 클라이언트 생성\n",
    "client = MultiServerMCPClient(\n",
    "    {\n",
    "        \"weather\": {\n",
    "            \"url\": \"http://localhost:8005/sse\",\n",
    "            \"transport\": \"sse\",\n",
    "        }\n",
    "    }\n",
    ")\n",
    "\n",
    "\n",
    "# 2. 명시적으로 연결 초기화 (이 부분이 필요함)\n",
    "# 초기화\n",
    "await client.__aenter__()\n",
    "\n",
    "# 이제 도구가 로드됨\n",
    "print(client.get_tools())  # 도구가 표시됨"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "langgraph 의 에이전트를 생성합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 에이전트 생성\n",
    "agent = create_react_agent(model, client.get_tools())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "그래프를 실행하여 결과를 확인합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "await astream_graph(agent, {\"messages\": \"서울의 날씨는 어떠니?\"})"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Stdio 통신 방식\n",
    "\n",
    "Stdio 통신 방식은 로컬 환경에서 사용하기 위해 사용합니다.\n",
    "\n",
    "- 통신을 위해 표준 입력/출력 사용\n",
    "\n",
    "참고: 아래의 python 경로는 수정하세요!"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from mcp import ClientSession, StdioServerParameters\n",
    "from mcp.client.stdio import stdio_client\n",
    "from langgraph.prebuilt import create_react_agent\n",
    "from langchain_mcp_adapters.tools import load_mcp_tools\n",
    "from langchain_anthropic import ChatAnthropic\n",
    "\n",
    "# Anthropic의 Claude 모델 초기화\n",
    "model = ChatAnthropic(\n",
    "    model_name=\"claude-3-7-sonnet-latest\", temperature=0, max_tokens=20000\n",
    ")\n",
    "\n",
    "# StdIO 서버 파라미터 설정\n",
    "# - command: Python 인터프리터 경로\n",
    "# - args: 실행할 MCP 서버 스크립트\n",
    "server_params = StdioServerParameters(\n",
    "    command=\"./.venv/bin/python\",\n",
    "    args=[\"mcp_server_local.py\"],\n",
    ")\n",
    "\n",
    "# StdIO 클라이언트를 사용하여 서버와 통신\n",
    "async with stdio_client(server_params) as (read, write):\n",
    "    # 클라이언트 세션 생성\n",
    "    async with ClientSession(read, write) as session:\n",
    "        # 연결 초기화\n",
    "        await session.initialize()\n",
    "\n",
    "        # MCP 도구 로드\n",
    "        tools = await load_mcp_tools(session)\n",
    "        print(tools)\n",
    "\n",
    "        # 에이전트 생성\n",
    "        agent = create_react_agent(model, tools)\n",
    "\n",
    "        # 에이전트 응답 스트리밍\n",
    "        await astream_graph(agent, {\"messages\": \"서울의 날씨는 어떠니?\"})"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## RAG 를 구축한 MCP 서버 사용\n",
    "\n",
    "- 파일: `mcp_server_rag.py`\n",
    "\n",
    "사전에 langchain 으로 구축한 `mcp_server_rag.py` 파일을 사용합니다.\n",
    "\n",
    "stdio 통신 방식으로 도구에 대한 정보를 가져옵니다. 여기서 도구는 `retriever` 도구를 가져오게 되며, 이 도구는 `mcp_server_rag.py` 에서 정의된 도구입니다. 이 파일은 사전에 서버에서 실행되지 **않아도** 됩니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from mcp import ClientSession, StdioServerParameters\n",
    "from mcp.client.stdio import stdio_client\n",
    "from langchain_mcp_adapters.tools import load_mcp_tools\n",
    "from langgraph.prebuilt import create_react_agent\n",
    "from langchain_anthropic import ChatAnthropic\n",
    "from utils import astream_graph\n",
    "\n",
    "# Anthropic의 Claude 모델 초기화\n",
    "model = ChatAnthropic(\n",
    "    model_name=\"claude-3-7-sonnet-latest\", temperature=0, max_tokens=20000\n",
    ")\n",
    "\n",
    "# RAG 서버를 위한 StdIO 서버 파라미터 설정\n",
    "server_params = StdioServerParameters(\n",
    "    command=\"./.venv/bin/python\",\n",
    "    args=[\"./mcp_server_rag.py\"],\n",
    ")\n",
    "\n",
    "# StdIO 클라이언트를 사용하여 RAG 서버와 통신\n",
    "async with stdio_client(server_params) as (read, write):\n",
    "    # 클라이언트 세션 생성\n",
    "    async with ClientSession(read, write) as session:\n",
    "        # 연결 초기화\n",
    "        await session.initialize()\n",
    "\n",
    "        # MCP 도구 로드 (여기서는 retriever 도구)\n",
    "        tools = await load_mcp_tools(session)\n",
    "\n",
    "        # 에이전트 생성 및 실행\n",
    "        agent = create_react_agent(model, tools)\n",
    "\n",
    "        # 에이전트 응답 스트리밍\n",
    "        await astream_graph(\n",
    "            agent, {\"messages\": \"삼성전자가 개발한 생성형 AI의 이름을 검색해줘\"}\n",
    "        )"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## SSE 방식과 StdIO 방식 혼합 사용\n",
    "\n",
    "- 파일: `mcp_server_rag.py` 는 StdIO 방식으로 통신\n",
    "- `langchain-dev-docs` 는 SSE 방식으로 통신\n",
    "\n",
    "SSE 방식과 StdIO 방식을 혼합하여 사용합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_mcp_adapters.client import MultiServerMCPClient\n",
    "from langgraph.prebuilt import create_react_agent\n",
    "from langchain_anthropic import ChatAnthropic\n",
    "\n",
    "# Anthropic의 Claude 모델 초기화\n",
    "model = ChatAnthropic(\n",
    "    model_name=\"claude-3-7-sonnet-latest\", temperature=0, max_tokens=20000\n",
    ")\n",
    "\n",
    "# 1. 다중 서버 MCP 클라이언트 생성\n",
    "client = MultiServerMCPClient(\n",
    "    {\n",
    "        \"document-retriever\": {\n",
    "            \"command\": \"./.venv/bin/python\",\n",
    "            # mcp_server_rag.py 파일의 절대 경로로 업데이트해야 합니다\n",
    "            \"args\": [\"./mcp_server_rag.py\"],\n",
    "            # stdio 방식으로 통신 (표준 입출력 사용)\n",
    "            \"transport\": \"stdio\",\n",
    "        },\n",
    "        \"langchain-dev-docs\": {\n",
    "            # SSE 서버가 실행 중인지 확인하세요\n",
    "            \"url\": \"https://teddynote.io/mcp/langchain/sse\",\n",
    "            # SSE(Server-Sent Events) 방식으로 통신\n",
    "            \"transport\": \"sse\",\n",
    "        },\n",
    "    }\n",
    ")\n",
    "\n",
    "\n",
    "# 2. 비동기 컨텍스트 매니저를 통한 명시적 연결 초기화\n",
    "await client.__aenter__()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "langgraph 의 `create_react_agent` 를 사용하여 에이전트를 생성합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [],
   "source": [
    "from langgraph.checkpoint.memory import MemorySaver\n",
    "from langchain_core.runnables import RunnableConfig\n",
    "\n",
    "prompt = (\n",
    "    \"You are a smart agent. \"\n",
    "    \"Use `retriever` tool to search on AI related documents and answer questions.\"\n",
    "    \"Use `langchain-dev-docs` tool to search on langchain / langgraph related documents and answer questions.\"\n",
    "    \"Answer in Korean.\"\n",
    ")\n",
    "agent = create_react_agent(\n",
    "    model, client.get_tools(), prompt=prompt, checkpointer=MemorySaver()\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "구축해 놓은 `mcp_server_rag.py` 에서 정의한 `retriever` 도구를 사용하여 검색을 수행합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "config = RunnableConfig(recursion_limit=30, thread_id=1)\n",
    "await astream_graph(\n",
    "    agent,\n",
    "    {\n",
    "        \"messages\": \"`retriever` 도구를 사용해서 삼성전자가 개발한 생성형 AI 이름을 검색해줘\"\n",
    "    },\n",
    "    config=config,\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "이번에는 `langchain-dev-docs` 도구를 사용하여 검색을 수행합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "config = RunnableConfig(recursion_limit=30, thread_id=1)\n",
    "await astream_graph(\n",
    "    agent,\n",
    "    {\"messages\": \"langgraph-dev-docs 참고해서 self-rag 의 정의에 대해서 알려줘\"},\n",
    "    config=config,\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "`MemorySaver` 를 사용하여 단기 기억을 유지합니다. 따라서, multi-turn 대화도 가능합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "await astream_graph(\n",
    "    agent, {\"messages\": \"이전의 내용을 bullet point 로 요약해줘\"}, config=config\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## LangChain 에 통합된 도구 + MCP 도구\n",
    "\n",
    "여기서는 LangChain 에 통합된 도구를 기존의 MCP 로만 이루어진 도구와 함께 사용이 가능한지 테스트 합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_community.tools.tavily_search import TavilySearchResults\n",
    "\n",
    "# Tavily 검색 도구를 초기화 합니다. (news 타입, 최근 3일 내 뉴스)\n",
    "tavily = TavilySearchResults(max_results=3, topic=\"news\", days=3)\n",
    "\n",
    "# 기존의 MCP 도구와 함께 사용합니다.\n",
    "tools = client.get_tools() + [tavily]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "langgraph 의 `create_react_agent` 를 사용하여 에이전트를 생성합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [],
   "source": [
    "from langgraph.checkpoint.memory import MemorySaver\n",
    "from langchain_core.runnables import RunnableConfig\n",
    "\n",
    "# 재귀 제한 및 스레드 아이디 설정\n",
    "config = RunnableConfig(recursion_limit=30, thread_id=2)\n",
    "\n",
    "# 프롬프트 설정\n",
    "prompt = \"You are a smart agent with various tools. Answer questions in Korean.\"\n",
    "\n",
    "# 에이전트 생성\n",
    "agent = create_react_agent(model, tools, prompt=prompt, checkpointer=MemorySaver())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "새롭게 추가한 `tavily` 도구를 사용하여 검색을 수행합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "await astream_graph(agent, {\"messages\": \"오늘 뉴스 찾아줘\"}, config=config)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "`retriever` 도구가 원활하게 작동하는 것을 확인할 수 있습니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "await astream_graph(\n",
    "    agent,\n",
    "    {\n",
    "        \"messages\": \"`retriever` 도구를 사용해서 삼성전자가 개발한 생성형 AI 이름을 검색해줘\"\n",
    "    },\n",
    "    config=config,\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Smithery 에서 제공하는 MCP 서버\n",
    "\n",
    "- 링크: https://smithery.ai/"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "사용한 도구 목록은 아래와 같습니다.\n",
    "\n",
    "- Sequential Thinking: https://smithery.ai/server/@smithery-ai/server-sequential-thinking\n",
    "  - 구조화된 사고 프로세스를 통해 역동적이고 성찰적인 문제 해결을 위한 도구를 제공하는 MCP 서버\n",
    "- Desktop Commander: https://smithery.ai/server/@wonderwhy-er/desktop-commander\n",
    "  - 다양한 편집 기능으로 터미널 명령을 실행하고 파일을 관리하세요. 코딩, 셸 및 터미널, 작업 자동화\n",
    "\n",
    "**참고**\n",
    "\n",
    "- smithery 에서 제공하는 도구를 JSON 형식으로 가져올때, 아래의 예시처럼 `\"transport\": \"stdio\"` 로 꼭 설정해야 합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_mcp_adapters.client import MultiServerMCPClient\n",
    "from langgraph.prebuilt import create_react_agent\n",
    "from langchain_anthropic import ChatAnthropic\n",
    "\n",
    "# LLM 모델 초기화\n",
    "model = ChatAnthropic(model=\"claude-3-7-sonnet-latest\", temperature=0, max_tokens=20000)\n",
    "\n",
    "# 1. 클라이언트 생성\n",
    "client = MultiServerMCPClient(\n",
    "    {\n",
    "        \"server-sequential-thinking\": {\n",
    "            \"command\": \"npx\",\n",
    "            \"args\": [\n",
    "                \"-y\",\n",
    "                \"@smithery/cli@latest\",\n",
    "                \"run\",\n",
    "                \"@smithery-ai/server-sequential-thinking\",\n",
    "                \"--key\",\n",
    "                \"89a4780a-53b7-4b7b-92e9-a29815f2669b\",\n",
    "            ],\n",
    "            \"transport\": \"stdio\",  # stdio 방식으로 통신을 추가합니다.\n",
    "        },\n",
    "        \"desktop-commander\": {\n",
    "            \"command\": \"npx\",\n",
    "            \"args\": [\n",
    "                \"-y\",\n",
    "                \"@smithery/cli@latest\",\n",
    "                \"run\",\n",
    "                \"@wonderwhy-er/desktop-commander\",\n",
    "                \"--key\",\n",
    "                \"89a4780a-53b7-4b7b-92e9-a29815f2669b\",\n",
    "            ],\n",
    "            \"transport\": \"stdio\",  # stdio 방식으로 통신을 추가합니다.\n",
    "        },\n",
    "        \"document-retriever\": {\n",
    "            \"command\": \"./.venv/bin/python\",\n",
    "            # mcp_server_rag.py 파일의 절대 경로로 업데이트해야 합니다\n",
    "            \"args\": [\"./mcp_server_rag.py\"],\n",
    "            # stdio 방식으로 통신 (표준 입출력 사용)\n",
    "            \"transport\": \"stdio\",\n",
    "        },\n",
    "    }\n",
    ")\n",
    "\n",
    "\n",
    "# 2. 명시적으로 연결 초기화\n",
    "await client.__aenter__()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "langgraph 의 `create_react_agent` 를 사용하여 에이전트를 생성합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {},
   "outputs": [],
   "source": [
    "from langgraph.checkpoint.memory import MemorySaver\n",
    "from langchain_core.runnables import RunnableConfig\n",
    "\n",
    "config = RunnableConfig(recursion_limit=30, thread_id=3)\n",
    "agent = create_react_agent(model, client.get_tools(), checkpointer=MemorySaver())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "`Desktop Commander` 도구를 사용하여 터미널 명령을 실행합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "await astream_graph(\n",
    "    agent,\n",
    "    {\n",
    "        \"messages\": \"현재 경로를 포함한 하위 폴더 구조를 tree 로 그려줘. 단, .venv 폴더는 제외하고 출력해줘.\"\n",
    "    },\n",
    "    config=config,\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "이번에는 `Sequential Thinking` 도구를 사용하여 비교적 복잡한 작업을 수행할 수 있는지 확인합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "await astream_graph(\n",
    "    agent,\n",
    "    {\n",
    "        \"messages\": (\n",
    "            \"`retriever` 도구를 사용해서 삼성전자가 개발한 생성형 AI 관련 내용을 검색하고 \"\n",
    "            \"`Sequential Thinking` 도구를 사용해서 보고서를 작성해줘.\"\n",
    "        )\n",
    "    },\n",
    "    config=config,\n",
    ")"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": ".venv",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.12.8"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
